Aller au contenu principal

REST API et HTTP en Flutter

Introduction aux API REST

Une API REST (Representational State Transfer) est une architecture permettant à des applications de communiquer via le protocole HTTP. Les APIs REST utilisent des méthodes HTTP standard pour effectuer des opérations CRUD (Create, Read, Update, Delete).

Méthodes HTTP principales

MéthodeOpérationUtilisation
GETLectureRécupérer des données
POSTCréationCréer une nouvelle ressource
PUTMise à jour complèteRemplacer une ressource
PATCHMise à jour partielleModifier une partie d'une ressource
DELETESuppressionSupprimer une ressource

Codes de statut HTTP

CodeSignificationExemple
200OKRequête réussie
201CreatedRessource créée avec succès
204No ContentSuppression réussie
400Bad RequestDonnées invalides
401UnauthorizedNon authentifié
403ForbiddenAccès refusé
404Not FoundRessource introuvable
500Internal Server ErrorErreur serveur

Le package http

Le package http est la bibliothèque standard pour effectuer des requêtes HTTP en Flutter.

Installation

Ajoutez la dépendance dans pubspec.yaml :

dependencies:
http: ^1.1.0

Puis installez :

flutter pub get

Import

import 'package:http/http.dart' as http;
import 'dart:convert'; // Pour JSON

Requêtes GET

Récupérer des données simples

Future<String> recupererDonnees() async {
final url = Uri.parse('https://api.example.com/data');

try {
final response = await http.get(url);

if (response.statusCode == 200) {
return response.body;
} else {
throw Exception('Erreur ${response.statusCode}: ${response.reasonPhrase}');
}
} catch (e) {
throw Exception('Erreur réseau: $e');
}
}

Avec en-têtes personnalisés

Future<String> recupererAvecHeaders() async {
final url = Uri.parse('https://api.example.com/data');
final headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN',
};

final response = await http.get(url, headers: headers);

if (response.statusCode == 200) {
return response.body;
} else {
throw Exception('Erreur ${response.statusCode}');
}
}

Avec paramètres de requête

Future<String> rechercherProduits(String query, int page) async {
final url = Uri.parse('https://api.example.com/products').replace(
queryParameters: {
'q': query,
'page': page.toString(),
'limit': '20',
},
);

// URL générée: https://api.example.com/products?q=flutter&page=1&limit=20

final response = await http.get(url);
return response.body;
}

Parsing JSON

JSON vers objet Dart

import 'dart:convert';

class Product {
final int id;
final String title;
final double price;
final String? description;

Product({
required this.id,
required this.title,
required this.price,
this.description,
});

// Constructeur factory pour créer un Product depuis JSON
factory Product.fromJson(Map<String, dynamic> json) {
return Product(
id: json['id'] as int,
title: json['title'] as String,
price: (json['price'] as num).toDouble(),
description: json['description'] as String?,
);
}

// Convertir un Product en JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'price': price,
'description': description,
};
}
}

// Récupérer un produit
Future<Product> recupererProduit(int id) async {
final url = Uri.parse('https://api.example.com/products/$id');
final response = await http.get(url);

if (response.statusCode == 200) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
return Product.fromJson(json);
} else {
throw Exception('Produit non trouvé');
}
}

// Récupérer une liste de produits
Future<List<Product>> recupererProduits() async {
final url = Uri.parse('https://api.example.com/products');
final response = await http.get(url);

if (response.statusCode == 200) {
final List<dynamic> jsonList = jsonDecode(response.body) as List;
return jsonList.map((json) => Product.fromJson(json as Map<String, dynamic>)).toList();
} else {
throw Exception('Erreur lors de la récupération');
}
}

Requêtes POST

Créer une ressource

Future<Product> creerProduit(Product product) async {
final url = Uri.parse('https://api.example.com/products');

try {
final response = await http.post(
url,
headers: {
'Content-Type': 'application/json',
},
body: jsonEncode(product.toJson()),
);

if (response.statusCode == 201) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
return Product.fromJson(json);
} else {
throw Exception('Erreur lors de la création: ${response.statusCode}');
}
} catch (e) {
throw Exception('Erreur réseau: $e');
}
}

// Utilisation
void main() async {
final nouveauProduit = Product(
id: 0, // Sera assigné par le serveur
title: 'Flutter Book',
price: 29.99,
description: 'Un excellent livre sur Flutter',
);

try {
final produitCree = await creerProduit(nouveauProduit);
print('Produit créé avec l\'ID: ${produitCree.id}');
} catch (e) {
print('Erreur: $e');
}
}

Requêtes PUT et PATCH

Mise à jour complète (PUT)

Future<Product> mettreAJourProduit(int id, Product product) async {
final url = Uri.parse('https://api.example.com/products/$id');

final response = await http.put(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode(product.toJson()),
);

if (response.statusCode == 200) {
return Product.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Erreur lors de la mise à jour');
}
}

Mise à jour partielle (PATCH)

Future<Product> modifierPrix(int id, double nouveauPrix) async {
final url = Uri.parse('https://api.example.com/products/$id');

final response = await http.patch(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'price': nouveauPrix}),
);

if (response.statusCode == 200) {
return Product.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Erreur lors de la modification');
}
}

Requête DELETE

Future<void> supprimerProduit(int id) async {
final url = Uri.parse('https://api.example.com/products/$id');

final response = await http.delete(url);

if (response.statusCode == 204 || response.statusCode == 200) {
print('Produit supprimé avec succès');
} else {
throw Exception('Erreur lors de la suppression');
}
}

Gestion avancée des erreurs

class ApiException implements Exception {
final int statusCode;
final String message;

ApiException(this.statusCode, this.message);


String toString() => 'ApiException($statusCode): $message';
}

class ApiService {
static const baseUrl = 'https://api.example.com';

Future<List<Product>> recupererProduits() async {
final url = Uri.parse('$baseUrl/products');

try {
final response = await http.get(url).timeout(
Duration(seconds: 10),
onTimeout: () {
throw TimeoutException('La requête a pris trop de temps');
},
);

return _handleResponse<List<Product>>(
response,
(json) {
final List<dynamic> list = json as List;
return list.map((item) => Product.fromJson(item as Map<String, dynamic>)).toList();
},
);
} on SocketException {
throw ApiException(0, 'Pas de connexion internet');
} on TimeoutException {
throw ApiException(0, 'Timeout de la requête');
} catch (e) {
throw ApiException(0, 'Erreur inconnue: $e');
}
}

T _handleResponse<T>(
http.Response response,
T Function(dynamic) parser,
) {
switch (response.statusCode) {
case 200:
case 201:
return parser(jsonDecode(response.body));
case 400:
throw ApiException(400, 'Requête invalide');
case 401:
throw ApiException(401, 'Non authentifié');
case 403:
throw ApiException(403, 'Accès refusé');
case 404:
throw ApiException(404, 'Ressource non trouvée');
case 500:
throw ApiException(500, 'Erreur serveur');
default:
throw ApiException(
response.statusCode,
'Erreur ${response.statusCode}: ${response.reasonPhrase}',
);
}
}
}

Client HTTP réutilisable

import 'dart:async';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'dart:convert';

class HttpClient {
final String baseUrl;
final Map<String, String> defaultHeaders;
final Duration timeout;

HttpClient({
required this.baseUrl,
this.defaultHeaders = const {},
this.timeout = const Duration(seconds: 10),
});

// GET
Future<T> get<T>(
String endpoint, {
Map<String, String>? headers,
Map<String, String>? queryParams,
T Function(dynamic)? parser,
}) async {
final uri = _buildUri(endpoint, queryParams);
final response = await http.get(
uri,
headers: {...defaultHeaders, ...?headers},
).timeout(timeout);

return _handleResponse(response, parser);
}

// POST
Future<T> post<T>(
String endpoint, {
required dynamic body,
Map<String, String>? headers,
T Function(dynamic)? parser,
}) async {
final uri = _buildUri(endpoint);
final response = await http.post(
uri,
headers: {
'Content-Type': 'application/json',
...defaultHeaders,
...?headers,
},
body: jsonEncode(body),
).timeout(timeout);

return _handleResponse(response, parser);
}

// PUT
Future<T> put<T>(
String endpoint, {
required dynamic body,
Map<String, String>? headers,
T Function(dynamic)? parser,
}) async {
final uri = _buildUri(endpoint);
final response = await http.put(
uri,
headers: {
'Content-Type': 'application/json',
...defaultHeaders,
...?headers,
},
body: jsonEncode(body),
).timeout(timeout);

return _handleResponse(response, parser);
}

// DELETE
Future<void> delete(
String endpoint, {
Map<String, String>? headers,
}) async {
final uri = _buildUri(endpoint);
final response = await http.delete(
uri,
headers: {...defaultHeaders, ...?headers},
).timeout(timeout);

if (response.statusCode < 200 || response.statusCode >= 300) {
throw HttpException('Erreur ${response.statusCode}');
}
}

Uri _buildUri(String endpoint, [Map<String, String>? queryParams]) {
final path = baseUrl.endsWith('/') ? baseUrl + endpoint : '$baseUrl/$endpoint';
return Uri.parse(path).replace(queryParameters: queryParams);
}

T _handleResponse<T>(http.Response response, T Function(dynamic)? parser) {
if (response.statusCode >= 200 && response.statusCode < 300) {
if (parser != null && response.body.isNotEmpty) {
return parser(jsonDecode(response.body));
}
return response.body as T;
} else {
throw HttpException('Erreur ${response.statusCode}: ${response.reasonPhrase}');
}
}
}

// Utilisation
final client = HttpClient(
baseUrl: 'https://api.example.com',
defaultHeaders: {
'Authorization': 'Bearer YOUR_TOKEN',
},
);

Future<List<Product>> recupererProduits() async {
return await client.get<List<Product>>(
'products',
parser: (json) {
final list = json as List;
return list.map((item) => Product.fromJson(item as Map<String, dynamic>)).toList();
},
);
}

Bonnes pratiques

  1. Gérer tous les cas d'erreur : Réseau, timeout, codes HTTP, parsing JSON
  2. Utiliser des timeouts : Éviter les attentes infinies
  3. Typer les réponses : Créer des classes modèles avec fromJson/toJson
  4. Centraliser les requêtes : Créer un service API réutilisable
  5. Logger les erreurs : Aider au débogage en production
  6. Utiliser des constantes : Pour les URLs et endpoints
  7. Tester avec des API publiques : JSONPlaceholder, Fake Store API
  8. Respecter les limites de taux : Rate limiting des APIs
  9. Cacher les données : Éviter les requêtes inutiles
  10. Authentification sécurisée : Ne pas stocker les tokens en clair

APIs publiques pour tester